﻿
Imports System.Net
Imports System.IO
Imports std
Imports xmlctrl
Imports xhtmlctrl
Imports webctrl

#Const DPSLIB = False

Public Module frontdoor
    Public ReadOnly httpContextParameter As httpContext.parameterDefinition = Nothing
    Public Const defaultConfigfile As String = "frontdoor.ini"
    Public Const maxHttpRequestRetryTimes As Int64 = 8
    Private WithEvents server As httpServer3 = Nothing
    Private WithEvents config As configMonitor = Nothing
    Private staticCache As cacheManager(Of String, bytes) = Nothing
    Private maintain As bytes = Nothing
    Private delayjumpDelay As Int64 = configNaming.defaultValue.frontdoor.delayjumpDelay
    Public holdtime As Int64 = configNaming.defaultValue.frontdoor.holdtime
    Private errorfile As bytes = Nothing
    Private rejectfile As bytes = Nothing

    Private Sub readInternalFile(ByVal configName As String, ByVal defaultValue As String,
                                 ByVal failback As String, ByRef output As bytes)
        Dim str As String = Nothing
        str = config.parameter(configNaming.frontdoor.name, configName, defaultValue)
        Try
            output = New bytes(File.ReadAllBytes(str))
        Catch ex As Exception
            raiseError("cannot read " + configName + " file " + str + ", " + ex.Message, errorHandle.errorType.warning)
            output = New bytes(encoding.utf8.GetBytes(failback))
        End Try
    End Sub

    Private Sub initial()
        assert(Not config Is Nothing, "config is nothing before startService")
        Dim ports As String
        ports = config.parameter(configNaming.frontdoor.name,
                                 configNaming.frontdoor.port,
                                 configNaming.defaultValue.frontdoor.port)
        server.addports(ports)

        Dim str As String = Nothing
        Dim i As Int64 = 0
        If config.getparameter(configNaming.frontdoor.name, configNaming.frontdoor.threadCount, str) AndAlso
           convertor.toint64(str, i) Then
            resolve(Of ithreadpool)().threadCount = i
        End If
        server.enableHDDoSLite(config, configNaming.frontdoor.name)
        server.addHDDoSLiteBypassPaths(FDInternalRequest.ping, FDInternalRequest.ping2)
        raiseError("server will start on ports " + Convert.ToString(ports) + _
                   " with " + Convert.ToString(resolve(Of ithreadpool)().threadCount) +
                   " threads, and enabled hddoslite.")

        staticCache = New cacheManager(Of String, bytes)(True,
                                       config.parameter(configNaming.frontdoor.name,
                                                        configNaming.frontdoor.staticCacheSize,
                                                        configNaming.defaultValue.frontdoor.staticCacheSize),
                                       config.parameter(configNaming.frontdoor.name,
                                                        configNaming.frontdoor.staticCacheLife,
                                                        configNaming.defaultValue.frontdoor.staticCacheLife))

        readInternalFile(configNaming.frontdoor.maintain, configNaming.defaultValue.frontdoor.maintain,
                         "maintaining...", maintain)
        readInternalFile(configNaming.frontdoor.error, configNaming.defaultValue.frontdoor.error,
                         "cannot serve this page.", errorfile)
        readInternalFile(configNaming.frontdoor.reject, configNaming.defaultValue.frontdoor.reject,
                         "rejected, seems you are requesting heavily.", rejectfile)
        str = config.parameter(configNaming.frontdoor.name, configNaming.frontdoor.maintain,
                               configNaming.defaultValue.frontdoor.maintain)

        holdtime = config.parameter(configNaming.frontdoor.name, configNaming.frontdoor.holdtime,
                                    configNaming.defaultValue.frontdoor.holdtime)
        If holdtime <= 0 Then
            raiseError("invalid holdtime in configuration, use default value.", errorHandle.errorType.warning)
            holdtime = configNaming.defaultValue.frontdoor.holdtime
        End If

        delayjumpDelay = config.parameter(configNaming.frontdoor.name, configNaming.frontdoor.delayjumpDelay,
                                          configNaming.defaultValue.frontdoor.delayjumpDelay)
    End Sub

    Private Sub shutdownService()
        If Not server Is Nothing Then
            server.stop()
        End If
        staticCache.clear()
    End Sub

    Private Sub startService()
        If server Is Nothing Then
            server = New httpServer3(httpContextParameter)
        Else
            server.stop()
        End If
        initial()
        server.start()
    End Sub

    Private Sub shutdown()
        shutdownService()
        If Not config Is Nothing Then
            config.close()
        End If
        finishmain()
    End Sub

    Private Sub monitor()
        While Not consoleExtSignal() AndAlso (config.configRefreshing() OrElse server.active())
            sleep(1000)
        End While
        If consoleExtSignal() Then
            raiseError("caught console cancel key event, shutdown ...")
        ElseIf Not server.active() Then
            raiseError("caught httpserver error.", errorHandle.errorType.critical)
        Else
            raiseError("unhandled error.", errorHandle.errorType.critical)
        End If
        shutdown()
    End Sub

    Public Sub unhandledException(ByVal ex As Exception)
        shutdown()
    End Sub

    Public Sub main(ByVal args() As String)
#If DPSLIB Then
        dpslib.consoleApplicationStartParameters.unhandledExceptionhandler = AddressOf unhandledException
        dpslib.startmain()
#Else
        startmain(AddressOf unhandledException)
#End If
        If Not isdebugbuild() Then
            errorHandle.skipErrorType += errorHandle.errorTypeChar(errorHandle.errorType.application)
        End If

        config = New configMonitor()
        Dim configfile As String = Nothing
        If args.Length() > 0 Then
            configfile = args(0)
        Else
            configfile = defaultConfigfile
        End If

        assert(config.open(configfile), "cannot open configuration file " + configfile)
        loosenSPMLimit(maxInt16)
        threadpool.register()
        callbackManager.register()
        startService()
        monitor()
    End Sub

    Private Sub config_ConfigChanged() Handles config.configChanged
        raiseError("caught configuration changed, will restart service.")
        shutdownService()
        startService()
    End Sub

    Private Function generatefilters(ByVal context As httpContext) As map(Of String, String)
        assert(Not context Is Nothing, "context is nothing.")
        Dim filters As map(Of String, String) = Nothing
        filters = context.generatefilters()
        If filters.find(filterNaming.urlhash) = filters.end() Then
            filters(filterNaming.urlhash) = signing(context.url().full)
            filters(filterNaming.pathqueryhash) = signing(context.url().pathquery)
            filters(filterNaming.randomvalue) = rnd(0, 100, True)
        End If

        Return filters
    End Function

    Private Function matchrules(ByVal context As httpContext, ByVal section As String _
                                , ByRef rtn As sortmap(Of String, String)) As Boolean
        rtn = config.section(section, generatefilters(context))
        Return Not rtn Is Nothing
    End Function

    Private Function matchrule(ByVal context As httpContext, ByVal section As String, ByRef rtn As String) As Boolean
        Return config.trygetparameter(section, context.url.pathquery, rtn, Nothing, generatefilters(context))
    End Function

    Private Function frontmatch(ByVal rule As String, ByVal pathquery As String) As Boolean
        Return strsame(rule, pathquery, strlen(rule), False)
    End Function

    Private Function frontmatch(ByVal rules As sortmap(Of String, String), ByVal pathquery As String) _
                                As sortmap(Of String, String).iterator
        Dim it As sortmap(Of String, String).iterator = Nothing
        it = rules.rbegin()
        'match longer rules first
        While it <> rules.rend()
            If frontmatch((+it).first, pathquery) Then
                Exit While
            End If
            it -= 1
        End While

        If it = rules.rend() Then
            Return rules.end()
        Else
            Return it
        End If
    End Function

    Private Function separate(Of firstT, secondT)(ByVal line As String _
                                                  , ByRef first As firstT, ByRef second As secondT _
                                                  , Optional ByVal separators As String = character.sheffer) As Boolean
        Dim temp As vector(Of String) = Nothing
        strsepar(line, separators, temp, character.quoteMark, False)
        If temp.size() = 0 Then
            Return False
        Else
            first = convertor.cast(Of firstT)(temp(0))
            If temp.size() > 1 Then
                second = convertor.cast(Of secondT)(temp(1))
            End If

            Return True
        End If
    End Function

    Private Function processRedirections(ByVal context As httpContext) As Boolean
        Try
            Dim redirections As sortmap(Of String, String) = Nothing
            If Not matchrules(context, configNaming.redirections.name, redirections) Then
                'raiseError("do not get matched redirection rules for context, request " + context.url.full _
                '           , errorHandle.errorType.warning)
                Return False
            End If

            Dim it As sortmap(Of String, String).iterator = Nothing
            it = frontmatch(redirections, context.url.pathquery)

            If it = redirections.end() OrElse it = redirections.rend() Then
                Return False
            Else
                If isdebugmode() Then
                    raiseError("fit redirection prefix of " + (+it).first + " for request " + context.url.full _
                               , errorHandle.errorType.application)
                End If
            End If

            Dim originDestination As String = Nothing
            Dim delayJump As Boolean = True
            If Not separate((+it).second, originDestination, delayJump) Then
                raiseError("cannot get destination and delayjump definition from configuration " + (+it).first _
                           + " " + (+it).second, errorHandle.errorType.warning)
                Return False
            End If

            Dim requestPathquery As String = Nothing
            requestPathquery = strmid(context.url.pathquery, strlen((+it).first))
            Dim destination As String = Nothing
            destination = originDestination + requestPathquery
            If delayJump AndAlso context.isBrowserUserAgent() Then
                Dim buff As MemoryStream = Nothing
                buff = context.buff()

                Dim delayRedirector As delayRedirector = Nothing
                delayRedirector = New delayRedirector(buff)
                delayRedirector.originalUrl = context.request.Url.ToString()
                delayRedirector.delay = delayjumpDelay
                delayRedirector.destination = destination

                If delayRedirector.render() Then
                    context.setstatuscode(HttpStatusCode.OK, "OK/SOFT30x")
                    context.setcontentType(network.http.contentType.texthtmlUtf8)
                    context.close()
                    Return True
                Else
                    raiseError("cannot render xhtmlpage when generating delayjump page.", errorHandle.errorType.warning)
                    Return False
                End If
            Else
                If Not context.redirect(destination) Then
                    raiseError("cannot redirect to destination " + destination + " for request " + context.url.full _
                               , errorHandle.errorType.warning)
                End If
                context.close()

                Return True
            End If
        Catch ex As Exception
            raiseError("caught unexpected error, request " + context.url().full() + ", " + ex.Message _
                       + ", " + ex.StackTrace, errorHandle.errorType.exclamation)
            Return False
        End Try
    End Function

    Private Function processStaticFile(ByVal context As httpContext) As Boolean
        Try
            Dim staticfile As String = Nothing
            If Not matchrule(context, configNaming.staticfiles.name, staticfile) Then
                'raiseError("do not get matched static file for context, request " + context.url.full _
                '            , errorHandle.errorType.warning)
                Return False
            Else
                If isdebugmode() Then
                    raiseError("fit static file of pathquery " + staticfile + " for request " + context.url.full _
                                , errorHandle.errorType.application)
                End If
            End If

            Dim filename As String = Nothing
            Dim contenttype As String = Nothing
            If Not separate(staticfile, filename, contenttype) Then
                raiseError("cannot get filename and contenttype from configuration " + staticfile _
                           , errorHandle.errorType.warning)
                Return False
            End If
            If isemptystring(contenttype) Then
                contenttype = contentTypes.getdefaultContentType(filename)
            End If

            Dim buff As bytes = Nothing
            If staticCache.cachesize <= 0 OrElse Not staticCache.get(filename, buff) Then
                buff = New bytes(File.ReadAllBytes(filename))
                If Not buff Is Nothing AndAlso staticCache.cachesize > 0 Then
                    staticCache.add(filename, buff)
                End If
            End If

            If Not buff Is Nothing Then
                context.buff.Write(+buff, 0, buff.size())
                context.setstatuscode(HttpStatusCode.OK, "OK")
                context.setcontentType(contenttype)
                context.setcacheControl(staticCache.cachesize() > 0, staticCache.retireticks())
                context.close()
                Return True
            Else
                raiseError("cannot read file from " + filename, errorHandle.errorType.warning)
                Return False
            End If
        Catch ex As Exception
            raiseError("caught unexpected error, request " + context.url().full() + ", " + ex.Message _
                       + ", " + ex.StackTrace, errorHandle.errorType.exclamation)
            Return False
        End Try
    End Function

    Private Function processRules(ByVal context As httpContext) As Boolean
        Try
            Dim rules As sortmap(Of String, String) = Nothing
            If Not matchrules(context, configNaming.rules.name, rules) Then
                'raiseError("do not get matched rules for context, request " + context.url.full _
                '           , errorHandle.errorType.warning)
                Return False
            End If

            Dim it As sortmap(Of String, String).iterator = Nothing
            it = frontmatch(rules, context.url.pathquery)

            Dim out_match As String = Nothing
            Dim rule As String = Nothing
            If it = rules.end() OrElse it = rules.rend() Then
                If isdebugbuild() Then
                    raiseError("no prefix fit, use DEFAULT for request " + context.url.full,
                               errorHandle.errorType.application)
                End If
                out_match = network.pathSeperator
                rule = rules(configNaming.rules.DEFAULT)
            Else
                If isdebugbuild() Then
                    debugRaiseError("fit rule prefix of " + (+it).first + " for request " + context.url.full,
                                    errorHandle.errorType.application)
                End If
                out_match = (+it).first
                rule = (+it).second
            End If

            If isemptystring(rule) Then
                Return False
            End If

            Dim destination As String = Nothing
            Dim needFDHeaders As Boolean = True
            If Not separate(rule, destination, needFDHeaders) Then
                raiseError("cannot get destination and needFDHeaders definition from configuration " + rule _
                            , errorHandle.errorType.warning)
                Return False
            End If

            Dim proxy As fdproxy = Nothing
            proxy = fdproxyPool().get()
            If proxy Is Nothing Then
                Return False
            Else
                proxy.initial(needFDHeaders)
                Dim action As callbackAction = Nothing
                If proxy.proxy(context, out_match, destination, action) Then
                    Dim action2 As callbackAction = Nothing
                    action2 = New callbackAction(check:=Function() As Boolean
                                                            Return action.finished()
                                                        End Function,
                                                 [end]:=Function() As Boolean
                                                            If action.end_result().nottrue() Then
                                                                Dim msg As String = Nothing
                                                                msg = "cannot get response from " + destination +
                                                                      " for request " + context.url().full +
                                                                      ", ignore request."
                                                                If Not proxy.exception() Is Nothing Then
                                                                    msg += character.blank + proxy.exception().Message
                                                                End If
                                                                raiseError(msg, errorHandle.errorType.warning)
                                                                processErrorContext(context)
                                                            Else
                                                                context.close()
                                                            End If
                                                            fdproxyPool().release(proxy)
                                                            Return True
                                                        End Function)
                    Return begin(action2)
                Else
                    fdproxyPool().release(proxy)
                    Return False
                End If
            End If
        Catch ex As Exception
            raiseError("caught unexpected error, request " + context.url().full() + ", " + ex.Message _
                       + ", " + ex.StackTrace, errorHandle.errorType.exclamation)
            Return False
        End Try
    End Function

    Private Function processInternalRequest(ByVal context As httpContext) As Boolean
        Try
            Dim buff As bytes = Nothing
            Dim match As Boolean = True
            With context.url()
                If strsame(.pathquery, FDInternalRequest.ver, False) Then
                    If isdebugmode() Then
                        raiseError("match internalRequest " + FDInternalRequest.ver _
                                   , errorHandle.errorType.application)
                    End If
                    buff = internalRequest.response.ver
                    context.setcacheControl(True, secondsToTicks(30 * 60))
                ElseIf strsame(.pathquery, FDInternalRequest.sample, False) Then
                    If isdebugmode() Then
                        raiseError("match internalRequest " + FDInternalRequest.sample _
                                   , errorHandle.errorType.application)
                    End If
                    buff = internalRequest.response.sample
                    context.setcacheControl(True, secondsToTicks(30 * 60))
                ElseIf strsame(.pathquery, FDInternalRequest.buildInfo, False) Then
                    If isdebugmode() Then
                        raiseError("match internalRequest " + FDInternalRequest.buildInfo _
                                   , errorHandle.errorType.application)
                    End If
                    buff = internalRequest.response.buildInfo
                    context.setcacheControl(True, secondsToTicks(30 * 60))
                ElseIf strsame(.pathquery, FDInternalRequest.diagnostic, False) Then
                    If isdebugmode() Then
                        raiseError("match internalRequest " + FDInternalRequest.diagnostic _
                                   , errorHandle.errorType.application)
                    End If
                    Dim page As xhtmlpage = Nothing
                    page = diagnostic.createXHtmlPage(context)
                    If Not page.render() Then
                        raiseError("cannot render diagnostic page.", errorHandle.errorType.warning)
                    End If
                    context.setcontentType(network.http.contentType.texthtmlUtf8)
                ElseIf strsame(.pathquery, FDInternalRequest.status, False) Then
                    If isdebugmode() Then
                        raiseError("match internalRequest " + FDInternalRequest.status _
                                   , errorHandle.errorType.application)
                    End If
                    Dim page As xhtmlpage = Nothing
                    page = status.createXHtmlPage(context)
                    If Not page.render() Then
                        raiseError("cannot render status page.", errorHandle.errorType.warning)
                    End If
                    context.setcontentType(network.http.contentType.texthtmlUtf8)
                ElseIf strsame(.path, FDInternalRequest.ping, False) OrElse _
                       strsame(.path, FDInternalRequest.ping2, False) Then
                    If isdebugmode() Then
                        raiseError("match internalRequest " + _
                                   FDInternalRequest.ping + " or " + FDInternalRequest.ping2, _
                                   errorHandle.errorType.application)
                    End If
                    nocontentProcessor.process2(context)
                ElseIf config.configRefreshing Then
                    raiseError("config is refreshing, return maintaining information." _
                               , errorHandle.errorType.application)
                    buff = maintain
                    context.setcacheControl(False)
                Else
                    match = False
                End If
            End With

            If match Then
                If Not buff Is Nothing Then
                    context.buff().Write(+buff, 0, buff.size())
                    context.setcontentType(network.http.contentType.textplainUtf8)
                End If
                If context.buff().Position() > 0 Then
                    context.setcontentEncoding(encoding.utf8)
                    context.setstatuscode(HttpStatusCode.OK, "OK")
                End If
                context.close()
                Return True
            Else
                Return False
            End If
        Catch ex As Exception
            raiseError("caught unexpected error, request " + context.url().full() + ", " + ex.Message _
                       + ", " + ex.StackTrace, errorHandle.errorType.exclamation)
            Return False
        End Try
    End Function

    Private Sub processErrorContextImpl(ByVal context As httpContext, ByVal bt As bytes)
        Try
            context.buff().Write(+bt, 0, bt.size())
            context.setcontentType(network.http.contentType.texthtmlUtf8)
            context.setcontentEncoding(encoding.utf8)
            context.setcacheControl(False)
        Catch ex As Exception
            raiseError("caught unexpected error, request " + context.url().full() + ", " + ex.Message _
                       + ", " + ex.StackTrace, errorHandle.errorType.exclamation)
        End Try
    End Sub

    Private Sub processErrorContext(ByVal context As httpContext)
        Try
            processErrorContextImpl(context, errorfile)
            context.setstatuscode(HttpStatusCode.NotFound, "404/SOFT_NOTFOUND")
            context.close()
        Catch ex As Exception
            raiseError("caught unexpected error, request " + context.url().full() + ", " + ex.Message _
                       + ", " + ex.StackTrace, errorHandle.errorType.exclamation)
        End Try
    End Sub

    Private Sub processRejectContext(ByVal context As httpContext)
        Try
            processErrorContextImpl(context, rejectfile)
            context.setstatuscode(HttpStatusCode.Forbidden, "403/REJECT")
            context.close()
        Catch ex As Exception
            raiseError("caught unexpected error, request " + context.url().full() + ", " + ex.Message _
                       + ", " + ex.StackTrace, errorHandle.errorType.exclamation)
        End Try
    End Sub

    Private Function requestInfo(ByVal context As httpContext) As String
        If debugassert(Not context Is Nothing, "context is nothing.") Then
            Return context.url().full() + " from " + context.requestIp() + _
                   ", referer " + context.friendlyReferer() + _
                   ", userAgent " + context.friendlyUserAgent() + _
                   ", traceId " + context.traceID()
        Else
            Return Nothing
        End If
    End Function

    Private Sub server_receive(ByVal context As httpContext) Handles server.receive
        Try
            raiseError("request " + requestInfo(context), errorHandle.errorType.application)
            If Not processInternalRequest(context) AndAlso _
               Not processRedirections(context) AndAlso _
               Not processStaticFile(context) AndAlso _
               Not processRules(context) Then
                processErrorContext(context)
            End If
        Catch ex As Exception
            raiseError("caught unexpected error, " + ex.Message, errorHandle.errorType.exclamation)
        End Try
    End Sub

    Private Sub server_reject(ByVal context As httpContext) Handles server.reject
        Try
            raiseError("reject request " + requestInfo(context) + ", reject issues " + context.rejectIssues(), _
                       errorHandle.errorType.application)
            processRejectContext(context)
        Catch ex As Exception
            raiseError("caught unexpected error, " + ex.Message, errorHandle.errorType.exclamation)
        End Try
    End Sub

    Sub New()
        'do not need to parse input stream by httpcontext, frontdoor handle the stream itself
        'but cannot confirm whether an FD is also work behind an FD, so leave FD headers
        httpContextParameter = New httpContext.parameterDefinition(parseInputStream:=False, _
                                                                   parsefdheaders:=True, _
                                                                   enableGzip:=False)
    End Sub
End Module
